feat(reports): add student engagement report API (#64) - #190
Open
Fury03 wants to merge 1 commit into
Open
Conversation
New GET /reports/engagement endpoint summarising student activity across STUDENT, COURSE (cohort) and PLATFORM scopes. - EngagementReportQueryDto validates scope + required id, ISO-8601 period and the inactivity threshold (1-365 days) - EngagementReportService aggregates watch time (LessonProgress.watchedSecs), lesson + course completion rates, average progress and learning streaks (UserPoints), and rolls per-enrollment activity up to the student to flag anyone idle past the threshold - guarded for ADMIN / INSTRUCTOR - spec drives the real controller->service->prisma path: scope + period validation, cohort scoping, aggregate maths, inactivity flagging and the empty-platform payload
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #64
Problem Statement (The Bug)
There was no student-engagement reporting surface at all. The platform
stores every input a learning-engagement view needs —
LessonProgress(
watchedSecs,completed,updatedAt),Enrollment(progressPercent,status),UserPoints(currentStreak,longestStreak) — but nothingreads them back as a summary. Admins and instructors have no way to answer
"how engaged is this cohort?" or "which students have gone quiet?".
This is not a local patch to an existing report: there is no
engagement-report.*module, no route, no aggregation code. It has to bebuilt as a first-class read model spanning three tables, with scope and
period validation, because ad-hoc per-table queries can't express
cross-table facts like "watch time per student" or "students inactive for N
days".
Solution Comparison and Decision
ReportsServiceAnalyticsEventis a raw fire-and-forget event log with no link to enrollments or lesson completion state. It cannot produce completion rates or progress averages without reconstructing state thatLessonProgressalready holds authoritatively.The Change
New DTO —
EngagementReportQueryDto(scope,studentId?,courseId?,from?,to?,inactiveDays):New core method —
EngagementReportService.getReport(dto). It validates,resolves the period, pulls the in-scope enrollments once, then fans out:
Inactivity flagging — per-enrollment last activity
(
max(enrollment.updatedAt, max(lessonProgress.updatedAt))) is rolled up tothe student and compared against
now - inactiveDays:JwtAuthGuard+RolesGuard,@Roles(ADMIN, INSTRUCTOR)GET /reports/engagement(new controller, mounted alongside the existing abuse-reports controller)studentIdrequired (400 if missing), student must exist (404), enrollment query filtered bystudentIdcourseIdrequired (400), course must exist (404), enrollment query filtered bycourseIdfrom/tomust be ISO-8601;from > to→ 400; applied toenrolledAtandlessonProgress.updatedAtsummarywith watch time, lesson + course completion rates, avg progress, streaks, inactivity blockgroupByskipped)Sample response:
{ "scope": "COURSE", "courseId": "course-1", "period": { "from": null, "to": null }, "generatedAt": "2026-08-27T12:00:00.000Z", "summary": { "students": 2, "enrollments": 2, "completedEnrollments": 1, "courseCompletionRate": 0.5, "avgProgressPercent": 70, "watchTime": { "totalSeconds": 7200, "totalHours": 2, "avgSecondsPerStudent": 3600 }, "lessons": { "started": 10, "completed": 6, "completionRate": 0.6 }, "streaks": { "avgCurrentStreak": 3, "longestStreak": 12 }, "inactivity": { "thresholdDays": 14, "inactiveStudents": 1, "inactiveRate": 0.5, "studentIds": ["stu-stale"], "truncated": false } } }Compatibility Note
No
INTERFACE_VERSIONconstant exists in this repo. The change is purelyadditive: one new route, one new DTO, one new service.
ReportsModulegains a controller/provider; the existing
ReportsController,ReportsServiceand/reportsroutes are untouched. No schema change, nomigration.
Incidental Fixes
ReportsModulenowexportsits providers, soEngagementReportServiceis reusable by other modules (matching the pattern the abuse
ReportsServicealready followed).rate()/round()helpers) so an empty scopereturns
0rather thanNaNin the JSON.Testing
src/modules/reports/engagement-report.service.spec.ts— resolves the realEngagementReportController+EngagementReportServicefrom a Nest module(Prisma mocked) and calls through
controller.getReport(...):rejects STUDENT scope without a studentId—FAILED(400) as required404s when the requested student does not existrejects COURSE scope without a courseIdrejects an inverted reporting period— the adversarialfrom > tocasescopes the enrollment query to the course for a cohort reportsummarises watch time, lesson and course completion for a cohort—asserts exact aggregate maths (2h watch time, 0.6 lesson completion,
avg streak 3, longest 12)
flags students whose last lesson activity is older than the threshold—the inactivity adversarial case (one stale, one fresh → exactly one flagged)
returns a well-formed zeroed payload when there is no dataPre-existing, unrelated TypeScript failures in
src/modules/reviews/*andsrc/modules/uploads/video-transcode.service.tsare present onmainandare not touched here.
Additional Notes
src/modules/reports/only — one DTO, one controller, oneservice, and the module wiring. No changes to the abuse-report code path,
to
prisma/schema.prisma, or to any other module. Instructor-performancereporting ([Backend] Create instructor performance report API #65) is deliberately a separate PR.